Vue进阶知识(2)
(一) webpack配置
(1) 前端项目构建打包工具介绍
- 为什么需要这些项目构建打包工具
- 项目构建打包工具有哪些
- grunt
- gulp(流)
- fis
- rollup
- webpack(万物皆模块)
- vite(快)
- webpack介绍
- 是一款打包构建工具,目前就流行打包构建工具
- webpack特点: 一切皆模块, 能把所有资源打包成浏览器能识别的 html,css,js,png
- 官网地址: https://webpack.docschina.org/
(2) 一份webpack配置
核心知识点:
- 入口和出口
- loader: webpack默认只认识js模块, 其它文件都要配置响应的loader
- plugin: 可以给webpack添加额外的功能
- resolve: 可以配置一写特殊的功能
- devServer 启动服务,也可在它里面配置跨域
const path = require("path");
const htmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/main.js",
output: {
filename: "app.js",
path: path.resolve(__dirname, "dist"),
},
mode: "development",
// 对模块进行配置
module: {
// 规则
rules: [
{
// 意思是: 遇到.css结尾的文件, 先使用style-loader和css-loader进行处理
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"],
},
],
},
plugins: [new htmlWebpackPlugin()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
// 省略后缀名
extensions: [".mjs", ".js", ".jsx", ".vue", ".json", ".wasm"],
},
devServer: {
compress: true,
open:true, //是否自动打开默认浏览器
port: 8080 // 端口号
}
};
(3) vue-cli是如何创建项目的
vue-cli底层使用的就是webpack来构建和打包项目的, 但是它把webpack的配置都隐藏起来了
查看vue-cli中的webpack配置
vue inspect > webpack.config.js
(三) vue配置跨域
// 代理
proxy: {
// 所有包含有'api'字符串的请求地址都会转发到target指向的地址
"/api": {
target: "http://81.71.65.4:3003",
ws: true,
// 允许跨域
changeOrigin: true,
pathRewrite: {
"^/api": "", //通过pathRewrite重写地址,将前缀/api转为/
},
},
},
(四) 统一管理请求
/src/api/index.js
import $axios from '../utils/request';
// account模块
export const $accountAdd = (data)=> {
return $axios.post('/account/add',data);
}
export const $accountList = ()=> {
return $axios.get('/account/list');
}
export const $accountLogin = (params)=> {
return $axios.post('/account/login',params);
}
export const $accountDel = (data={})=> {
return $axios.post('/account/del',data);
使用
<script>
import * as api from "../../api/index";
export default {
data() {
return {
list: [],
};
},
created() {
this.getList();
},
methods: {
getList() {
api.$feeUsageList().then((res) => {
this.list = res.data;
});
},
}
};
</script>
(五) 路由守卫
常常用来鉴权(鉴察权限)
// 路由白名单
const whiteList = ["/login"];
/**
* 路由守卫
* to 要前往的路由
* from 当前路由
* next 下一步操作
*/
router.beforeEach((to, from, next) => {
if (store.state.token) {
//已经登录: 如果路由是登录页,默认跳转到首页
if (to.path == "/login") {
next({
path: "/dashboard/index",
});
} else {
next();
}
} else {
//没有登录,路由是否在白名单中
if (whiteList.includes(to.path)) {
//放行
next();
} else {
//没在白名单
next("/login");
}
}
});
export default router;
(六) 用户权限管理
根据不同角色配置不同的路由数组
注: 有些公司由后台配置, 但由前端来配置更灵活更方便
登录获取token
根据用户角色通过router.addRoutes动态添加用户角色对应的路由数组, 并把数组保存到store里
从store里获取路由数组, 动态渲染侧边栏
(七) 自定义vue指令
一个简单的vue指令(是字体变红色)
<template>
<div>
<span v-red> welcome</span>
</div>
</template>
<script>
export default {
directives: {
red: {
// el就是使用v-red指令的那个标签
inserted(el) {
el.style.color = "red";
},
},
},
};
</script>
vue指令的三个构造函数:
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用
(八) vue双向数据绑定原理
https://zhuanlan.zhihu.com/p/29079569
此问题回答的关键点:
- 双向数据绑定和兴原理
- vue双向数据绑定的实现过程
(1) 双向数据绑定核心原理
vue数据双向绑定是通过es5的一个新增的一个特性Object.defineProperty来对数据进行劫持, 然后结合发布者-订阅者模式的方式来实现的,其中比较关键的是数据劫持(Object.defineProperty),下面咱们看一个例子。
var obj = {}
Object.defineProperty(obj, 'name', {
get: function () {
console.log("获取了");
},
set: function () {
console.log('修改了');
}
})
// 修改属性
obj.name = 'fei';
// 读取属性
var name = obj.name;
有了Object.defineProperty, 我们就可以在获取和修改属性的时候做一些响应的操作, 从而实现数据的双向绑定, 下面是一个简单的版的双向数据绑定
<body>
<div id="app">
<input type="text" oninput="handleInput()" id="inp">
<p id="text"></p>
</div>
<script>
var obj = {};
var $inp = document.querySelector('#inp');
var $text = document.querySelector('#text');
Object.defineProperty(obj, 'username', {
set: function (value) {
$inp.value = value;
$text.innerText = value;
},
})
function handleInput() {
obj.username = $inp.value;;
}
</script>
</body>
(2) Vue的双向数据绑定原理
实现过程
我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此接下去我们执行以下3个步骤,实现数据的双向绑定:
实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
手写实现vue双向数据绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vue双向数据绑定</title>
</head>
<body>
<div id="app">
{{username}} <br>
<input type="text" v-model="msg"> <br>
{{ msg }}
</div>
<script>
/**
* 第1步: 实现一个监听器Observer
* 用来劫持并监听所有属性
* 如果属性有变动的,就通知订阅者。
*/
function Observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
function defineReactive(obj, key, val) {
var dep = new Dep();
// 劫持数据
Object.defineProperty(obj, key, {
get: function () {
// 添加订阅者 watcher 到主题对象 Dep
if (Dep.target) dep.addSub(Dep.target);
return val
},
set: function (newVal) {
// 新数据和旧数据相同, 不做处理
if (newVal === val) return;
val = newVal;
// 作为发布者发出通知
dep.notify();
}
});
}
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
var child;
// 许多同学反应看不懂这一段,这里有必要解释一下
// 首先,所有表达式必然会返回一个值,赋值表达式亦不例外
// 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法
// 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
// 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
while (child = node.firstChild) {
compile(child, vm);
flag.appendChild(child); // 将子节点劫持到文档片段中
}
return flag
}
/**
* 第2步: 实现一个订阅者Watcher
* 可以收到属性的变化通知并执行相应的函数,从而更新视图。
*/
function Watcher(vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
// 调用更新视图的方法去更新视图
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
// 获取 data 中的属性值
get: function () {
this.value = this.vm[this.name]; // 触发相应属性的 get
}
}
// 因为订阅者可能有很多个, 所以需要一个消息收集器
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
}
/**
* 第3步: 实现一个解析器Compile
* 可以扫描和解析每个节点的相关指令
* 并根据初始化模板数据以及初始化相应的订阅器。
*/
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/;
// 节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的 set 方法
vm[name] = e.target.value;
});
node.value = vm[name]; // 将 data 的值赋给该 node
node.removeAttribute('v-model');
}
};
new Watcher(vm, node, name, 'input');
}
// 节点类型为 text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
new Watcher(vm, node, name, 'text');
}
}
}
function Vue(options) {
this.data = options.data;
var data = this.data;
Observe(data, this);
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将 dom 返回到 app 中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
username: '张三',
msg: 'hello world'
}
})
</script>
</body>
</html>
(九) keep-alive
用户在某个列表页面选择筛选条件过滤出一份数据列表,由列表页面进入数据详情页面,再返回该列表页面,我们希望:列表页面可以保留用户的筛选(或选中)状态。
keep-alive就是用来解决这种场景。当然keep-alive不仅仅是能够保存页面/组件的状态这么简单,它还可以避免组件反复创建和渲染,有效提升系统性能。总的来说,keep-alive用于保存组件的渲染状态。
keep-alive的生命周期
- 初次进入时:created > mounted > activated;退出后触发 deactivated
- 再次进入:会触发 activated;事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中
应用实例
注意: 要把keep-alive放在父路由上, 不然不生效
// 例子1: 缓存所有组件
<keep-alive> <router-view/> </keep-alive>
// 例子2: 缓存部分组件
// router.js { path: "/demo", name: "demo", meta: { title: "demo", icon: "icon-d-right-arrow", }, hidden: isDev ? false : true, component: Layout, redirect: "/demo/index", children: [ { path: "index", name: "demo-index", meta: {keepAlive: true, // true代表需要缓存,必须放入meta里, 不然访问不到, component: () => import("@/views/demo/index.vue"), }, ], }, <!-- 父路由组件(在这里是Layout)添加一下keep-alive --> <!-- 对keepAlive为true的路由使用keep-alive包裹 --> <keep-alive> <router-view v-if="$route.meta.keepAlive" /> </keep-alive> <router-view v-if="!$route.meta.keepAlive" />
总结:
- 被缓存的组件, created和mounted只会调用一次, 第二次进入组件不再调用(强制刷新除外)
- 被缓存的组件有额外的两个生命周期activated和deactivated, 如果你进入或离开组件需要做一些操作, 可以在这两个生命周期里进行
(十) 组件通信方式总结
(1) 父子组件通信(略)
(2) vuex跨组件通信(略)
(3) provide和inject 祖宗和后代通信
- 祖宗使用provide定义数据或方法
- 后代(任何层级)都可以使用inject获取数据
// 祖宗组件, App.vue
<script>
export default {
provide: {
username: '张三',
say() {
console.log(this.username+'是狂徒');
}
}
}
</script>
// 后代组件
<template>
<div>provide和inject</div>
</template>
<script>
export default {
inject: ["username", "say"],
created() {
console.log(this.username);
this.say();
},
};
</script>
(3) vue中央事件总线机制(bus)
vue中非父子组件之间通信除了使用vuex,也可以通过bus总线,两者适用场景不同。
vuex适用中大型项目、数据在多组件之间公用的情况。
bus的本质是创建了一个空的vue实例用来存放数据, 适合小项目、数据被更少组件使用的项目,对于中大型项目 数据在很多组件之间使用的情况 bus就不太适用了。bus其实就是一个发布订阅模式,利用vue的自定义事件机制,在触发的地方通过$emit向外发布一个事件,在需要监听的页面,通过$on监听事件(订阅)。
注意:
- 需要先订阅, 发布的时候才能收到
- 比较适合兄弟组件通信, 也就是一个组件有很多子组件, 这些子组件之间的通信
- 也适合孙子组件发送消息个祖宗组件, 因为祖宗组件会比后代组件先加载
概念: 发布订阅模式, 就好像以前一个家庭跟一个报社订报纸, 报社是发布者, 有了新的报纸就派人去送报纸, 家庭是订阅者, 订了的家庭就能收到新报纸, 一个发布者可以对应多个订阅者
应用:
(1) main.js 导入事件中线插件, 需要先安装插件
import VueBus from 'vue-bus';
Vue.use(VueBus);
(2) demo1 子传父(孙传爷也一样)
- 父组件订阅事件
- 子组件发布事件
<template>
<div>
<h3>父组件 {{ username }}</h3>
<hr>
<Son/>
</div>
</template>
<script>
import Son from "./Son.vue";
export default {
components: {
Son,
},
data() {
return {
username: "张三",
};
},
created() {
this.$bus.on("aaa", (data) => {
this.username = data;
});
},
};
</script>
<template>
<div>
<button @click="sendEvent">发布事件</button>
</div>
</template>
<script>
import Son from './Son.vue'
export default {
components: {
Son,
},
methods: {
sendEvent() {
// 发布事件,名称为listEvent
this.$bus.emit("aaa", '李四');
},
},
};
</script>
(2) demo2 跨组件通信(非直属亲属关系)
- demo.vue订阅aaa事件
- demo2.vue发布aaa事件
其实不是太适合这种情况, 因为订阅一方得先运行, 而且还不能被销毁(要使用keep-alive)
<!-- demo.vue -->
<template>
<div>
<h3>demo1</h3>
<button @click="sendEvent">发布事件</button>
</div>
</template>
<script>
export default {
methods: {
sendEvent() {
alert('事件发布成功');
this.$bus.emit("aaa", "李四");
},
},
};
</script>
<!-- demo2.vue -->
<template>
<div>
<h3>demo2 {{ username }}</h3>
</div>
</template>
<script>
export default {
data() {
return {
username: "张三",
};
},
created() {
this.$bus.on("aaa", (data) => {
this.username = data;
});
},
};
</script>